var grapher = grapher || {};
var dagre = dagre || require('./dagre');

grapher.Graph = class {
  constructor(modifier, compound, options) {
    this.modifier = modifier;
    this._isCompound = compound;
    this._options = options;
    this._nodes = new Map();
    this._edges = new Map();
    this._children = {};
    this._children['\x00'] = {};
    this._parent = {};

    // this._pathArgumentNames = new Set();  // the name of arguments which occurs in both sides of an edge
  }

  get options() {
    return this._options;
  }

  setNode(node) {
    const key = node.name; // node id
    const value = this._nodes.get(key);
    if (value) {
      value.label = node;
    } else {
      this._nodes.set(key, { v: key, label: node });
      if (this._isCompound) {
        this._parent[key] = '\x00';
        this._children[key] = {};
        this._children['\x00'][key] = true;
      }
    }

    const modelNodeName = node.modelNodeName;
    this.modifier.name2ViewNode.set(modelNodeName, node);
    this.modifier.name2ModelNode.set(modelNodeName, node.value);

    // this.modifier.name2NodeStates save our modifications, and wil be initilized at the first graph construction only
    // otherwise the modfications will lost
    if (!this.modifier.name2NodeStates.get(modelNodeName)) {
      this.modifier.name2NodeStates.set(modelNodeName, 'Exist');
    }
  }

  setEdge(edge) {
    if (!this._nodes.has(edge.v)) {
      throw new grapher.Error();
    }
    if (!this._nodes.has(edge.w)) {
      throw new grapher.Error();
    }
    const key = edge.v + ':' + edge.w;
    if (!this._edges.has(key)) {
      this._edges.set(key, { v: edge.v, w: edge.w, label: edge });
    }

    // My code
    // _namedEdges: from : to
    var from_node_name = edge.from.modelNodeName;
    var to_node_name = edge.to.modelNodeName;
    // if (!this._namedEdges.has(from_node_name)) {
    //     this._namedEdges.set(from_node_name, []);
    // }
    // this._namedEdges.get(from_node_name).push(to_node_name);
    if (!this.modifier.namedEdges.has(from_node_name)) {
      this.modifier.namedEdges.set(from_node_name, []);
    }
    this.modifier.namedEdges.get(from_node_name).push(to_node_name);
  }

  setParent(node, parent) {
    if (!this._isCompound) {
      throw new Error('Cannot set parent in a non-compound graph');
    }
    parent += '';
    for (let ancestor = parent; ancestor; ancestor = this.parent(ancestor)) {
      if (ancestor === node) {
        throw new Error('Setting ' + parent + ' as parent of ' + node + ' would create a cycle');
      }
    }
    delete this._children[this._parent[node]][node];
    this._parent[node] = parent;
    this._children[parent][node] = true;
    return this;
  }

  get nodes() {
    return this._nodes;
  }

  hasNode(key) {
    return this._nodes.has(key);
  }

  node(key) {
    return this._nodes.get(key);
  }

  get edges() {
    return this._edges;
  }

  parent(key) {
    if (this._isCompound) {
      const parent = this._parent[key];
      if (parent !== '\x00') {
        return parent;
      }
    }
  }

  children(key) {
    key = key === undefined ? '\x00' : key;
    if (this._isCompound) {
      const children = this._children[key];
      if (children) {
        return Object.keys(children);
      }
    } else if (key === '\x00') {
      return this.nodes.keys();
    } else if (this.hasNode(key)) {
      return [];
    }
  }

  build(document, origin) {
    const createGroup = (name) => {
      const element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
      element.setAttribute('id', name);
      element.setAttribute('class', name);
      origin.appendChild(element);
      return element;
    };

    const clusterGroup = createGroup('clusters');
    const edgePathGroup = createGroup('edge-paths');
    const edgeLabelGroup = createGroup('edge-labels');
    const nodeGroup = createGroup('nodes');

    // ====> 显示 边上的箭头
    const edgePathGroupDefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
    edgePathGroup.appendChild(edgePathGroupDefs);
    const marker = (id) => {
      const element = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
      element.setAttribute('id', id);
      element.setAttribute('viewBox', '0 0 10 10');
      element.setAttribute('refX', 9);
      element.setAttribute('refY', 5);
      element.setAttribute('markerUnits', 'strokeWidth');
      element.setAttribute('markerWidth', 8);
      element.setAttribute('markerHeight', 6);
      element.setAttribute('orient', 'auto');
      const markerPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      markerPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 L 4 5 z');
      markerPath.style.setProperty('stroke-width', 1);
      element.appendChild(markerPath);
      return element;
    };
    edgePathGroupDefs.appendChild(marker('arrowhead-vee'));
    edgePathGroupDefs.appendChild(marker('arrowhead-vee-select'));
    // <==== 显示 边上的箭头

    for (const nodeId of this.nodes.keys()) {
      const node = this.node(nodeId);
      if (this.children(nodeId).length == 0) {
        if (this.modifier.name2NodeStates.get(node.label.modelNodeName) == 'Exist') {
          node.label.build(document, nodeGroup);
        }
      } else {
        // cluster
        node.label.rectangle = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        if (node.label.rx) {
          node.label.rectangle.setAttribute('rx', node.rx);
        }
        if (node.label.ry) {
          node.label.rectangle.setAttribute('ry', node.ry);
        }
        node.label.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        node.label.element.setAttribute('class', 'cluster');
        node.label.element.appendChild(node.label.rectangle);
        clusterGroup.appendChild(node.label.element);
      }
    }

    for (const edge of this.edges.values()) {
      var node_from = this._nodes.get(edge.v).label;
      var node_to = this._nodes.get(edge.w).label;
      if (
        this.modifier.name2NodeStates.get(node_from.modelNodeName) == 'Exist' &&
        this.modifier.name2NodeStates.get(node_to.modelNodeName) == 'Exist'
      ) {
        edge.label.build(document, edgePathGroup, edgeLabelGroup);
      }
    }
  }

  update() {
    dagre.layout(this);
    for (const nodeId of this.nodes.keys()) {
      const node = this.node(nodeId);
      if (this.children(nodeId).length == 0) {
        // node
        if (this.modifier.name2NodeStates.get(node.label.modelNodeName) == 'Exist') {
          node.label.update(); // 让节点显示出来
        }
      } else {
        // cluster
        const node = this.node(nodeId);
        node.label.element.setAttribute('transform', 'translate(' + node.label.x + ',' + node.label.y + ')');
        node.label.rectangle.setAttribute('x', -node.label.width / 2);
        node.label.rectangle.setAttribute('y', -node.label.height / 2);
        node.label.rectangle.setAttribute('width', node.label.width);
        node.label.rectangle.setAttribute('height', node.label.height);
      }
    }
    for (const edge of this.edges.values()) {
      var node_from = this._nodes.get(edge.v).label;
      var node_to = this._nodes.get(edge.w).label;
      if (
        this.modifier.name2NodeStates.get(node_from.modelNodeName) == 'Exist' &&
        this.modifier.name2NodeStates.get(node_to.modelNodeName) == 'Exist'
      ) {
        edge.label.update(); // 让边显示出来
      }
    }
  }
};

grapher.Node = class {
  constructor() {
    this._blocks = [];
  }

  header() {
    const block = new grapher.Node.Header();
    this._blocks.push(block);
    return block;
  }

  list() {
    const block = new grapher.Node.List();
    this._blocks.push(block);
    return block;
  }

  canvas() {
    const block = new grapher.Node.Canvas();
    this._blocks.push(block);
    return block;
  }

  build(document, parent) {
    this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    if (this.id) {
      this.element.setAttribute('id', this.id);
    }
    this.element.setAttribute('class', this.class ? 'node ' + this.class : 'node');
    this.element.classList.add('graph-node-hide');
    parent.appendChild(this.element);

    // ===> 配置每个节点的框边界
    this.border = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    this.border.setAttribute('class', ['node', 'border'].join(' '));
    this.element.appendChild(this.border);
    // <=== 配置每个节点的框边界

    for (let i = 0; i < this._blocks.length; i++) {
      const block = this._blocks[i];
      block.first = i === 0;
      block.last = i === this._blocks.length - 1;
      block.build(document, this.element);
    }

    this.layout();
  }

  layout() {
    const width = Math.max(...this._blocks.map((block) => block.width));
    let height = 0;
    for (let i = 0; i < this._blocks.length; i++) {
      const block = this._blocks[i];
      block.y = height;
      block.update(this.element, height, width, i == 0, i == this._blocks.length - 1);
      height = height + block.height;
    }

    // 这一行画每个节点的框边界
    this.border.setAttribute('d', grapher.Node.roundedRect(0, 0, width, height, true, true, true, true));

    const nodeBox = this.element.getBBox();
    this.width = nodeBox.width;
    this.height = nodeBox.height;
  }

  update() {
    this.element.setAttribute(
      'transform',
      'translate(' + (this.x - this.width / 2) + ',' + (this.y - this.height / 2) + ')'
    );

    // 设定不透明度
    this.element.classList.remove('graph-node-hide');
  }

  static roundedRect(x, y, width, height, r1, r2, r3, r4) {
    const radius = 5;
    r1 = r1 ? radius : 0;
    r2 = r2 ? radius : 0;
    r3 = r3 ? radius : 0;
    r4 = r4 ? radius : 0;
    return (
      'M' +
      (x + r1) +
      ',' +
      y +
      'h' +
      (width - r1 - r2) +
      'a' +
      r2 +
      ',' +
      r2 +
      ' 0 0 1 ' +
      r2 +
      ',' +
      r2 +
      'v' +
      (height - r2 - r3) +
      'a' +
      r3 +
      ',' +
      r3 +
      ' 0 0 1 ' +
      -r3 +
      ',' +
      r3 +
      'h' +
      (r3 + r4 - width) +
      'a' +
      r4 +
      ',' +
      r4 +
      ' 0 0 1 ' +
      -r4 +
      ',' +
      -r4 +
      'v' +
      (-height + r4 + r1) +
      'a' +
      r1 +
      ',' +
      r1 +
      ' 0 0 1 ' +
      r1 +
      ',' +
      -r1 +
      'z'
    );
  }
};

grapher.Node.Header = class {
  constructor() {
    this._entries = [];
  }

  add(id, classList, content, tooltip, handler) {
    const entry = new grapher.Node.Header.Entry(id, classList, content, tooltip, handler);
    this._entries.push(entry);
    return entry;
  }

  build(document, parent) {
    this._document = document;
    this.width = 0;
    this.height = 0;
    let x = 0;
    const y = 0;
    for (const entry of this._entries) {
      entry.x = x;
      entry.y = y;
      entry.build(document, parent);
      x += entry.width;
      this.height = Math.max(entry.height, this.height);
      this.width = Math.max(x, this.width);
    }
    if (!this.first) {
      this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
      parent.appendChild(this.line);
    }
  }

  update(parent, top, width) {
    const document = this._document;
    const dx = width - this.width;
    for (let i = 0; i < this._entries.length; i++) {
      const entry = this._entries[i];
      if (i == 0) {
        entry.width = entry.width + dx;
      } else {
        entry.x = entry.x + dx;
        entry.tx = entry.tx + dx;
      }
      entry.y = entry.y + top;
    }
    for (let i = 0; i < this._entries.length; i++) {
      const entry = this._entries[i];
      entry.element.setAttribute('transform', 'translate(' + entry.x + ',' + entry.y + ')');
      const r1 = i == 0 && this.first;
      const r2 = i == this._entries.length - 1 && this.first;
      const r3 = i == this._entries.length - 1 && this.last;
      const r4 = i == 0 && this.last;
      entry.path.setAttribute('d', grapher.Node.roundedRect(0, 0, entry.width, entry.height, r1, r2, r3, r4));
      entry.text.setAttribute('x', 6);
      entry.text.setAttribute('y', entry.ty);
    }
    for (let i = 0; i < this._entries.length; i++) {
      const entry = this._entries[i];
      if (i != 0) {
        const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        line.setAttribute('class', 'node');
        line.setAttribute('x1', entry.x);
        line.setAttribute('x2', entry.x);
        line.setAttribute('y1', top);
        line.setAttribute('y2', top + this.height);
        parent.appendChild(line);
      }
    }
    if (this.line) {
      this.line.setAttribute('class', 'node');
      this.line.setAttribute('x1', 0);
      this.line.setAttribute('x2', width);
      this.line.setAttribute('y1', top);
      this.line.setAttribute('y2', top);
    }
  }
};

grapher.Node.Header.Entry = class {
  constructor(id, classList, content, tooltip, handler) {
    this.id = id;
    this.classList = classList;
    this.content = content;
    this.tooltip = tooltip;
    this.handler = handler;
    this.events = {};
  }

  on(event, callback) {
    this.events[event] = this.events[event] || [];
    this.events[event].push(callback);
  }

  raise(event, data) {
    if (this.events && this.events[event]) {
      for (const callback of this.events[event]) {
        callback(this, data);
      }
    }
  }

  build(document, parent) {
    const yPadding = 4;
    const xPadding = 7;
    this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    parent.appendChild(this.element);
    this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    this.text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    this.element.appendChild(this.path);
    this.element.appendChild(this.text);
    const classList = ['node-item'];
    if (this.classList) {
      classList.push(...this.classList);
    }
    this.element.setAttribute('class', classList.join(' '));
    if (this.id) {
      this.element.setAttribute('id', this.id);
    }
    if (this.events.click) {
      this.element.addEventListener('click', () => this.raise('click'));
    }
    if (this.events.dblclick) {
      this.element.addEventListener('dblclick', () => this.raise('dblclick'));
    }

    if (this.tooltip) {
      const titleElement = document.createElementNS('http://www.w3.org/2000/svg', 'title');
      titleElement.textContent = this.tooltip;
      this.element.appendChild(titleElement);
    }
    if (this.content) {
      this.text.textContent = this.content;
    }
    const boundingBox = this.text.getBBox();
    this.width = boundingBox.width + xPadding + xPadding;
    this.height = boundingBox.height + yPadding + yPadding;
    this.tx = xPadding;
    this.ty = yPadding - boundingBox.y;
  }
};

grapher.Node.List = class {
  constructor() {
    this._items = [];
    this.events = {};
  }

  add(id, name, value, tooltip, separator) {
    const item = new grapher.Node.List.Item(id, name, value, tooltip, separator);
    this._items.push(item);
    return item;
  }

  on(event, callback) {
    this.events[event] = this.events[event] || [];
    this.events[event].push(callback);
  }

  raise(event, data) {
    if (this.events && this.events[event]) {
      for (const callback of this.events[event]) {
        callback(this, data);
      }
    }
  }

  build(document, parent) {
    this._document = document;
    this.width = 0;
    this.height = 0;
    const x = 0;
    const y = 0;
    this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    this.element.setAttribute('class', 'node-attribute');
    if (this.events.click) {
      this.element.addEventListener('click', () => this.raise('click'));
    }
    if (this.events.dblclick) {
      this.element.addEventListener('dblclick', () => this.raise('dblclick'));
    }
    this.element.setAttribute('transform', 'translate(' + x + ',' + y + ')');
    this.background = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    this.element.appendChild(this.background);
    parent.appendChild(this.element);
    this.height += 3;
    for (const item of this._items) {
      const yPadding = 1;
      const xPadding = 6;
      const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
      if (item.id) {
        text.setAttribute('id', item.id);
      }
      text.setAttribute('xml:space', 'preserve');
      this.element.appendChild(text);
      if (item.tooltip) {
        const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
        title.textContent = item.tooltip;
        text.appendChild(title);
      }
      const name = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
      name.textContent = item.name;
      if (item.separator.trim() != '=') {
        name.style.fontWeight = 'bold';
      }
      text.appendChild(name);
      const textValueElement = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
      textValueElement.textContent = item.separator + item.value;
      text.appendChild(textValueElement);
      const size = text.getBBox();
      const width = xPadding + size.width + xPadding;
      this.width = Math.max(width, this.width);
      text.setAttribute('x', x + xPadding);
      text.setAttribute('y', this.height + yPadding - size.y);
      this.height += yPadding + size.height + yPadding;
    }
    this.height += 3;
    this.width = Math.max(75, this.width);
    if (!this.first) {
      this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
      this.line.setAttribute('class', 'node');
      this.element.appendChild(this.line);
    }
  }

  update(parent, top, width) {
    this.element.setAttribute('transform', 'translate(0,' + this.y + ')');
    this.background.setAttribute(
      'd',
      grapher.Node.roundedRect(0, 0, width, this.height, this.first, this.first, this.last, this.last)
    );
    if (this.line) {
      this.line.setAttribute('x1', 0);
      this.line.setAttribute('x2', width);
      this.line.setAttribute('y1', 0);
      this.line.setAttribute('y2', 0);
    }
  }
};

grapher.Node.List.Item = class {
  constructor(id, name, value, tooltip, separator) {
    this.id = id;
    this.name = name;
    this.value = value;
    this.tooltip = tooltip;
    this.separator = separator;
  }
};

grapher.Node.Canvas = class {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  build(/* document, parent */) {}

  update(/* parent, top, width , first, last */) {}
};

grapher.Edge = class {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  get arrowhead() {
    return 'vee';
  }

  build(document, edgePathGroupElement, edgeLabelGroupElement) {
    const createElement = (name) => {
      return document.createElementNS('http://www.w3.org/2000/svg', name);
    };

    // 生成path对应的element
    this.element = createElement('path');
    if (this.id) {
      this.element.setAttribute('id', this.id);
    }
    this.element.setAttribute('class', this.class ? 'edge-path ' + this.class : 'edge-path');
    edgePathGroupElement.appendChild(this.element);

    // 生成label对应的element
    if (this.label) {
      const tspan = createElement('tspan');
      tspan.setAttribute('xml:space', 'preserve');
      tspan.setAttribute('dy', '1em');
      tspan.setAttribute('x', '1');
      tspan.appendChild(document.createTextNode(this.label));
      this.labelElement = createElement('text');
      this.labelElement.appendChild(tspan);
      this.labelElement.classList.add('graph-node-hide');
      this.labelElement.setAttribute('class', 'edge-label');
      if (this.id) {
        this.labelElement.setAttribute('id', 'edge-label-' + this.id);
      }
      edgeLabelGroupElement.appendChild(this.labelElement);
      const edgeBox = this.labelElement.getBBox();
      this.width = edgeBox.width;
      this.height = edgeBox.height;
    }
  }

  update() {
    const edgePath = grapher.Edge._computeCurvePath(this, this.from, this.to);
    this.element.setAttribute('d', edgePath); //  ===> 把边画出来

    // ===> 让label显示出来
    if (this.labelElement) {
      this.labelElement.setAttribute(
        'transform',
        'translate(' + (this.x - this.width / 2) + ',' + (this.y - this.height / 2) + ')'
      );
      this.labelElement.classList.remove('graph-node-hide');
    }
    // <=== 让label显示出来
  }

  static _computeCurvePath(edge, tail, head) {
    const points = edge.points.slice(1, edge.points.length - 1);
    points.unshift(grapher.Edge._intersectRect(tail, points[0]));
    points.push(grapher.Edge._intersectRect(head, points[points.length - 1]));
    const curve = new grapher.Edge.Curve(points);
    return curve.path.data;
  }

  static _intersectRect(node, point) {
    const x = node.x;
    const y = node.y;
    const dx = point.x - x;
    const dy = point.y - y;
    let w = node.width / 2;
    let h = node.height / 2;
    let sx;
    let sy;
    if (Math.abs(dy) * w > Math.abs(dx) * h) {
      if (dy < 0) {
        h = -h;
      }
      sx = dy === 0 ? 0 : (h * dx) / dy;
      sy = h;
    } else {
      if (dx < 0) {
        w = -w;
      }
      sx = w;
      sy = dx === 0 ? 0 : (w * dy) / dx;
    }
    return {
      x: x + sx,
      y: y + sy,
    };
  }
};

grapher.Edge.Curve = class {
  constructor(points) {
    this._path = new grapher.Edge.Path();
    this._x0 = NaN;
    this._x1 = NaN;
    this._y0 = NaN;
    this._y1 = NaN;
    this._state = 0;
    for (let i = 0; i < points.length; i++) {
      const point = points[i];
      this.point(point.x, point.y);
      if (i === points.length - 1) {
        switch (this._state) {
          case 3:
            this.curve(this._x1, this._y1);
            this._path.lineTo(this._x1, this._y1);
            break;
          case 2:
            this._path.lineTo(this._x1, this._y1);
            break;
        }
        if (this._line || (this._line !== 0 && this._point === 1)) {
          this._path.closePath();
        }
        this._line = 1 - this._line;
      }
    }
  }

  get path() {
    return this._path;
  }

  point(x, y) {
    x = +x;
    y = +y;
    switch (this._state) {
      case 0:
        this._state = 1;
        if (this._line) {
          this._path.lineTo(x, y);
        } else {
          this._path.moveTo(x, y);
        }
        break;
      case 1:
        this._state = 2;
        break;
      case 2:
        this._state = 3;
        this._path.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6);
        this.curve(x, y);
        break;
      default:
        this.curve(x, y);
        break;
    }
    this._x0 = this._x1;
    this._x1 = x;
    this._y0 = this._y1;
    this._y1 = y;
  }

  curve(x, y) {
    this._path.bezierCurveTo(
      (2 * this._x0 + this._x1) / 3,
      (2 * this._y0 + this._y1) / 3,
      (this._x0 + 2 * this._x1) / 3,
      (this._y0 + 2 * this._y1) / 3,
      (this._x0 + 4 * this._x1 + x) / 6,
      (this._y0 + 4 * this._y1 + y) / 6
    );
  }
};

grapher.Edge.Path = class {
  constructor() {
    this._x0 = null;
    this._y0 = null;
    this._x1 = null;
    this._y1 = null;
    this._data = '';
  }

  moveTo(x, y) {
    this._data += 'M' + (this._x0 = this._x1 = +x) + ',' + (this._y0 = this._y1 = +y);
  }

  lineTo(x, y) {
    this._data += 'L' + (this._x1 = +x) + ',' + (this._y1 = +y);
  }

  bezierCurveTo(x1, y1, x2, y2, x, y) {
    this._data += 'C' + +x1 + ',' + +y1 + ',' + +x2 + ',' + +y2 + ',' + (this._x1 = +x) + ',' + (this._y1 = +y);
  }

  closePath() {
    if (this._x1 !== null) {
      this._x1 = this._x0;
      this._y1 = this._y0;
      this._data += 'Z';
    }
  }

  get data() {
    return this._data;
  }
};

if (typeof module !== 'undefined' && typeof module.exports === 'object') {
  module.exports.Graph = grapher.Graph;
  module.exports.Node = grapher.Node;
  module.exports.Edge = grapher.Edge;
}
